/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
 */
package org.olap4j.query;

import org.olap4j.OlapException;
import org.olap4j.impl.IdentifierParser;
import org.olap4j.mdx.IdentifierSegment;
import org.olap4j.metadata.*;

import java.util.*;

/**
 * Usage of a dimension for an OLAP query.
 * 
 * <p>
 * It references an {@link org.olap4j.metadata.Dimension} and allows the query creator to manage the member selections
 * for the dimension. The state of a QueryDimension does not affect the Dimension object in any way so a single
 * Dimension object can be referenced by many QueryDimension objects.
 * 
 * @author jdixon, jhyde, Luc Boudreau
 * @since May 29, 2007
 */
public class QueryDimension extends QueryNodeImpl {
	protected QueryAxis axis;
	protected final List<Selection> inclusions = new SelectionList();
	protected final List<Selection> exclusions = new SelectionList();
	private final Query query;
	protected Dimension dimension;
	private SortOrder sortOrder = null;
	private HierarchizeMode hierarchizeMode = null;
	private boolean hierarchyConsistent = false;

	public QueryDimension(Query query, Dimension dimension) {
		super();
		this.query = query;
		this.dimension = dimension;
	}

	public Query getQuery() {
		return query;
	}

	public void setAxis(QueryAxis axis) {
		this.axis = axis;
	}

	public QueryAxis getAxis() {
		return axis;
	}

	public String getName() {
		return dimension.getName();
	}

	/**
	 * Selects members and includes them in the query.
	 * 
	 * <p>
	 * This method selects and includes a single member with the {@link Selection.Operator#MEMBER} operator.
	 * 
	 * @param nameParts
	 *            Name of the member to select and include.
	 * @throws OlapException
	 *             If no member corresponding to the supplied name parts could be resolved in the cube.
	 */
	public Selection include(List<IdentifierSegment> nameParts) throws OlapException {
		return this.include(Selection.Operator.MEMBER, nameParts);
	}

	public Selection createSelection(List<IdentifierSegment> nameParts) throws OlapException {
		return this.createSelection(Selection.Operator.MEMBER, nameParts);
	}

	/**
	 * Selects members and includes them in the query.
	 * 
	 * <p>
	 * This method selects and includes a member along with its relatives, depending on the supplied
	 * {@link Selection.Operator} operator.
	 * 
	 * @param operator
	 *            Selection operator that defines what relatives of the supplied member name to include along.
	 * @param nameParts
	 *            Name of the root member to select and include.
	 * @throws OlapException
	 *             If no member corresponding to the supplied name parts could be resolved in the cube.
	 */
	public Selection include(Selection.Operator operator, List<IdentifierSegment> nameParts) throws OlapException {
		Member member = this.getQuery().getCube().lookupMember(nameParts);
		if (member == null) {
			throw new OlapException("Unable to find a member with name " + nameParts);
		}
		return this.include(operator, member);
	}

	public Selection createSelection(Selection.Operator operator, List<IdentifierSegment> nameParts)
			throws OlapException {
		Member member = this.getQuery().getCube().lookupMember(nameParts);
		if (member == null) {
			throw new OlapException("Unable to find a member with name " + nameParts);
		}
		return this.createSelection(operator, member);
	}

	/**
	 * Selects members and includes them in the query.
	 * <p>
	 * This method selects and includes a single member with the {@link Selection.Operator#MEMBER} selection operator.
	 * 
	 * @param member
	 *            The member to select and include in the query.
	 */
	public Selection include(Member member) {
		return include(Selection.Operator.MEMBER, member);
	}

	public Selection createSelection(Member member) {
		return createSelection(Selection.Operator.MEMBER, member);
	}

	/**
	 * Selects a level and includes it in the query.
	 * <p>
	 * This method selects and includes a all members of the given query using the {@link Selection.Operator#MEMBERS}
	 * selection operator.
	 * 
	 * @param level
	 *            The level to select and include in the query.
	 */
	public Selection include(Level level) {
		if (level.getDimension().equals(this.dimension)) {
			Selection selection = query.getSelectionFactory().createLevelSelection(level);
			this.include(selection);
			return selection;
		}
		return null;
	}

	/**
	 * Selects members and includes them in the query.
	 * <p>
	 * This method selects and includes a member along with it's relatives, depending on the supplied
	 * {@link Selection.Operator} operator.
	 * 
	 * @param operator
	 *            Selection operator that defines what relatives of the supplied member name to include along.
	 * @param member
	 *            Root member to select and include.
	 */
	public Selection createSelection(Selection.Operator operator, Member member) {
		if (member.getDimension().equals(this.dimension)) {
			Selection selection = query.getSelectionFactory().createMemberSelection(member, operator);
			return selection;
		}
		return null;
	}

	/**
	 * Selects level and includes all members in the query.
	 * <p>
	 * This method selects and includes all members of a given Level, using the MEMBERS operator
	 * {@link Selection.Operator}
	 * 
	 * @param level
	 *            Root level to select and include.
	 */
	public Selection createSelection(Level level) {
		if (level.getDimension().equals(this.dimension)) {
			Selection selection = query.getSelectionFactory().createLevelSelection(level);
			return selection;
		}
		return null;
	}

	/**
	 * Selects members and includes them in the query.
	 * <p>
	 * This method selects and includes a member along with it's relatives, depending on the supplied
	 * {@link Selection.Operator} operator.
	 * 
	 * @param operator
	 *            Selection operator that defines what relatives of the supplied member name to include along.
	 * @param member
	 *            Root member to select and include.
	 */
	public Selection include(Selection.Operator operator, Member member) {
		if (member.getDimension().equals(this.dimension)) {
			Selection selection = query.getSelectionFactory().createMemberSelection(member, operator);
			this.include(selection);
			return selection;
		}
		return null;
	}

	/**
	 * Includes a selection of members in the query.
	 * 
	 * @param selection
	 *            The selection of members to include.
	 */
	private void include(Selection selection) {
		this.getInclusions().add(selection);
		Integer index = Integer.valueOf(this.getInclusions().indexOf(selection));
		this.notifyAdd(selection, index);
	}

	/**
	 * Clears the current member inclusions from this query dimension.
	 */
	public void clearInclusions() {
		Map<Integer, QueryNode> removed = new HashMap<Integer, QueryNode>();
		for (Selection node : this.inclusions) {
			removed.put(Integer.valueOf(this.inclusions.indexOf(node)), node);
			((QueryNodeImpl) node).clearListeners();
		}
		this.inclusions.clear();
		this.notifyRemove(removed);
	}

	/**
	 * Selects members and excludes them from the query.
	 * 
	 * <p>
	 * This method selects and excludes a single member with the {@link Selection.Operator#MEMBER} operator.
	 * 
	 * @param nameParts
	 *            Name of the member to select and exclude.
	 * @throws OlapException
	 *             If no member corresponding to the supplied name parts could be resolved in the cube.
	 */
	public void exclude(List<IdentifierSegment> nameParts) throws OlapException {
		this.exclude(Selection.Operator.MEMBER, nameParts);
	}

	/**
	 * Selects members and excludes them from the query.
	 * 
	 * <p>
	 * This method selects and excludes a member along with its relatives, depending on the supplied
	 * {@link Selection.Operator} operator.
	 * 
	 * @param operator
	 *            Selection operator that defines what relatives of the supplied member name to exclude along.
	 * @param nameParts
	 *            Name of the root member to select and exclude.
	 * @throws OlapException
	 *             If no member corresponding to the supplied name parts could be resolved in the cube.
	 */
	public void exclude(Selection.Operator operator, List<IdentifierSegment> nameParts) throws OlapException {
		Member rootMember = this.getQuery().getCube().lookupMember(nameParts);
		if (rootMember == null) {
			throw new OlapException("Unable to find a member with name " + nameParts);
		}
		this.exclude(operator, rootMember);
	}

	/**
	 * Selects level members and excludes them from the query.
	 * <p>
	 * This method selects and excludes members of a level with the {@link Selection.Operator#MEMBERS} selection
	 * operator.
	 * 
	 * @param level
	 *            The level to select and exclude from the query.
	 */
	public void exclude(Level level) {
		if (level.getDimension().equals(this.dimension)) {
			Selection selection = query.getSelectionFactory().createLevelSelection(level);
			this.exclude(selection);
		}
	}

	/**
	 * Selects members and excludes them from the query.
	 * <p>
	 * This method selects and excludes a single member with the {@link Selection.Operator#MEMBER} selection operator.
	 * 
	 * @param member
	 *            The member to select and exclude from the query.
	 */
	public void exclude(Member member) {
		exclude(Selection.Operator.MEMBER, member);
	}

	/**
	 * Selects members and excludes them from the query.
	 * <p>
	 * This method selects and excludes a member along with it's relatives, depending on the supplied
	 * {@link Selection.Operator} operator.
	 * 
	 * @param operator
	 *            Selection operator that defines what relatives of the supplied member name to exclude along.
	 * @param member
	 *            Root member to select and exclude.
	 */
	public void exclude(Selection.Operator operator, Member member) {
		if (member.getDimension().equals(this.dimension)) {
			Selection selection = query.getSelectionFactory().createMemberSelection(member, operator);
			this.exclude(selection);
		}
	}

	/**
	 * Excludes a selection of members from the query.
	 * 
	 * @param selection
	 *            The selection of members to exclude.
	 */
	private void exclude(Selection selection) {
		this.getExclusions().add(selection);
		Integer index = Integer.valueOf(this.getExclusions().indexOf(selection));
		this.notifyAdd(selection, index);
	}

	/**
	 * Clears the current member inclusions from this query dimension.
	 */
	public void clearExclusions() {
		Map<Integer, QueryNode> removed = new HashMap<Integer, QueryNode>();
		for (Selection node : this.exclusions) {
			removed.put(Integer.valueOf(this.exclusions.indexOf(node)), node);
			((QueryNodeImpl) node).clearListeners();
		}
		this.exclusions.clear();
		this.notifyRemove(removed);
	}

	/**
	 * Resolves a selection of members into an actual list of the root member and it's relatives selected by the
	 * Selection object.
	 * 
	 * @param selection
	 *            The selection of members to resolve.
	 * @return A list of the actual members selected by the selection object.
	 * @throws OlapException
	 *             If resolving the selections triggers an exception while looking up members in the underlying cube.
	 */
	public List<Member> resolve(Selection selection) throws OlapException {
		assert selection != null;
		final Member.TreeOp op;
		Member.TreeOp secondOp = null;
		switch (selection.getOperator()) {
		case CHILDREN:
			op = Member.TreeOp.CHILDREN;
			break;
		case SIBLINGS:
			op = Member.TreeOp.SIBLINGS;
			break;
		case INCLUDE_CHILDREN:
			op = Member.TreeOp.SELF;
			secondOp = Member.TreeOp.CHILDREN;
			break;
		case MEMBER:
			op = Member.TreeOp.SELF;
			break;
		default:
			throw new OlapException("Operation not supported: " + selection.getOperator());
		}
		Set<Member.TreeOp> set = new TreeSet<Member.TreeOp>();
		set.add(op);
		if (secondOp != null) {
			set.add(secondOp);
		}
		try {
			return query.getCube().lookupMembers(set, IdentifierParser.parseIdentifier(selection.getUniqueName()));
		} catch (Exception e) {
			throw new OlapException("Error while resolving selection " + selection.toString(), e);
		}
	}

	/**
	 * Returns a list of the inclusions within this dimension.
	 * 
	 * <p>
	 * Be aware that modifications to this list might have unpredictable consequences.
	 * </p>
	 * 
	 * @return list of inclusions
	 */
	public List<Selection> getInclusions() {
		return inclusions;
	}

	/**
	 * Returns a list of the exclusions within this dimension.
	 * 
	 * <p>
	 * Be aware that modifications to this list might have unpredictable consequences.
	 * </p>
	 * 
	 * @return list of exclusions
	 */
	public List<Selection> getExclusions() {
		return exclusions;
	}

	/**
	 * Returns the underlying dimension object onto which this query dimension is based.
	 * <p>
	 * Returns a mutable object so operations on it have unpredictable consequences.
	 * 
	 * @return The underlying dimension representation.
	 */
	public Dimension getDimension() {
		return dimension;
	}

	/**
	 * Forces a change onto which dimension is the current base of this QueryDimension object.
	 * <p>
	 * Forcing a change in the duimension assignment has unpredictable consequences.
	 * 
	 * @param dimension
	 *            The new dimension to assign to this query dimension.
	 */
	public void setDimension(Dimension dimension) {
		this.dimension = dimension;
	}

	/**
	 * Sorts the dimension members by name in the order supplied as a parameter.
	 * 
	 * @param order
	 *            The {@link SortOrder} to use.
	 */
	public void sort(SortOrder order) {
		this.sortOrder = order;
	}

	/**
	 * Returns the current order in which the dimension members are sorted.
	 * 
	 * @return A value of {@link SortOrder}
	 */
	public SortOrder getSortOrder() {
		return this.sortOrder;
	}

	/**
	 * Clears the current sorting settings.
	 */
	public void clearSort() {
		this.sortOrder = null;
	}

	/**
	 * Returns the current mode of hierarchization, or null if no hierarchization is currently performed.
	 * 
	 * <p>
	 * This capability is only available when a single dimension is selected on an axis
	 * 
	 * @return Either a hierarchization mode value or null if no hierarchization is currently performed.
	 */
	public HierarchizeMode getHierarchizeMode() {
		return hierarchizeMode;
	}

	/**
	 * Triggers the hierarchization of the included members within this QueryDimension.
	 * 
	 * <p>
	 * The dimension inclusions will be wrapped in an MDX Hierarchize function call.
	 * 
	 * <p>
	 * This capability is only available when a single dimension is selected on an axis.
	 * 
	 * @param hierarchizeMode
	 *            If parents should be included before or after their children. (Equivalent to the POST/PRE MDX literal
	 *            for the Hierarchize() function) inside the Hierarchize() MDX function call.
	 */
	public void setHierarchizeMode(HierarchizeMode hierarchizeMode) {
		this.hierarchizeMode = hierarchizeMode;
	}

	/**
	 * Tells the QueryDimension not to hierarchize its included selections.
	 * 
	 * <p>
	 * This capability is only available when a single dimension is selected on an axis.
	 */
	public void clearHierarchizeMode() {
		this.hierarchizeMode = null;
	}

	/**
	 * Tells the QueryDimension not to keep a consistent hierarchy within the inclusions when the mdx is generated. Only
	 * members whose Ancestors are included will be included.
	 * 
	 * <p>
	 * It uses the MDX function FILTER() in combination with ANCESTOR() to produce a set like:<br />
	 * <br />
	 * {[Time].[1997]}, <br />
	 * Filter({{[Time].[Quarter].Members}}, (Ancestor([Time].CurrentMember, [Time].[Year]) IN {[Time].[1997]}))
	 */
	public void setHierarchyConsistent(boolean consistent) {
		this.hierarchyConsistent = consistent;
	}

	/**
	 * Tells the QueryDimension not to keep a consistent hierarchy
	 */
	public boolean isHierarchyConsistent() {
		return this.hierarchyConsistent;
	}

	private class SelectionList extends AbstractList<Selection> {
		private final List<Selection> list = new ArrayList<Selection>();

		public Selection get(int index) {
			return list.get(index);
		}

		public int size() {
			return list.size();
		}

		public Selection set(int index, Selection selection) {
			return list.set(index, selection);
		}

		public void add(int index, Selection selection) {
			if (this.contains(selection)) {
				throw new IllegalStateException("dimension already contains selection");
			}
			list.add(index, selection);
		}

		public Selection remove(int index) {
			return list.remove(index);
		}
	}

	/**
	 * Defines in which way the hierarchize operation should be performed.
	 */
	public static enum HierarchizeMode {
		/**
		 * Parents are placed before children.
		 */
		PRE,
		/**
		 * Parents are placed after children
		 */
		POST
	}

	void tearDown() {
		for (Selection node : this.inclusions) {
			((QueryNodeImpl) node).clearListeners();
		}
		for (Selection node : this.exclusions) {
			((QueryNodeImpl) node).clearListeners();
		}
		this.inclusions.clear();
		this.exclusions.clear();
	}
}

// End QueryDimension.java

